Skip to content

Add system diagnostics, power controls, and WiFi radio toggle to Tools tab#389

Merged
ChuckBuilds merged 4 commits into
mainfrom
claude/tools-tab-terminal-hfk9f3
Jul 9, 2026
Merged

Add system diagnostics, power controls, and WiFi radio toggle to Tools tab#389
ChuckBuilds merged 4 commits into
mainfrom
claude/tools-tab-terminal-hfk9f3

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Expands the web UI Tools tab with safe, purpose-built controls so users can manage the Pi without SSHing in. This grew out of a request for an in-browser terminal, which was ruled out as too risky (the web UI has no auth and CSRF is disabled, so an arbitrary-command shell would be a remote-code-execution surface). Instead, this wires up a curated set of controls — most of which lean on capabilities that already exist in the backend.

  • System Diagnostics card — renders the existing-but-previously-unused GET /api/v3/system/status endpoint (CPU, memory, temperature, disk, uptime), with a manual Refresh button and a lightweight 10s poll.
  • System Power section — Reboot / Shutdown buttons wired to the existing reboot_system / shutdown_system actions, behind a confirm step. A dedicated powerAction() helper treats the dropped connection as the expected "going offline" outcome instead of a red error.
  • Network Radio section — a WiFi on/off toggle backed by new GET/POST /api/v3/wifi/radio endpoints and WiFiManager.set_wifi_radio() / get_wifi_radio_state(). Turning WiFi off is refused unless a wired (Ethernet) connection is present (reusing the existing lockout guards), with an explicit "force off anyway" confirmation for advanced users.

Bluetooth toggle was intentionally omitted: the installer removes the BlueZ stack because it interferes with LED matrix timing, so there'd be nothing to toggle on a standard install.

Type of change

  • New feature

Related issues

Test plan

  • Ran on a real Raspberry Pi with hardware
  • Ran in emulator mode (EMULATOR=true python3 run.py)
  • Manually verified the affected code path in the web UI

Verified against a locally-running web server (off-Pi, so nmcli/vcgencmd absent — confirms graceful degradation):

  • GET /api/v3/system/status returns the full diagnostics payload consumed by the new card.
  • GET /api/v3/wifi/radio{available:false, enabled:null, ethernet_connected:false}.
  • POST /api/v3/wifi/radio {"enabled":false} is refused with the anti-lockout message (HTTP 400); POST {}enabled is required (400).
  • GET /v3/partials/tools returns HTTP 200 with all new sections; the inline JS passes node --check; the Python modules compile.
  • test/web_interface/test_systemctl_sudoers_alignment.py passes (2/2) — no new privileged commands were introduced (uses the already-allowlisted nmcli radio wifi on|off and the existing reboot/poweroff grants).

Documentation

  • I added/updated docstrings on new public functions
  • N/A — no README/docs behavior changes beyond the new UI controls

Plugin compatibility

  • No plugin breakage expected (additive UI sections + new endpoints; no changes to BasePlugin, the loader, or the config schema)

Checklist

  • My commits follow the message convention
  • I've not committed any secrets or hardcoded API keys
  • N/A — no new config key added

Notes for reviewer

  • The WiFi-off guard relies on _has_connectivity_safety() / _is_ethernet_connected(); on a Pi reachable only via WiFi, disabling is blocked unless the user explicitly forces it (which will drop their connection — that's the documented trade-off).
  • Diagnostics/reboot/shutdown intentionally add no new backend capability — they surface endpoints and actions that already existed but were unreachable from the UI, so they inherit the current (unauthenticated, local-network) threat model rather than widening it.

🤖 Generated with Claude Code

https://claude.ai/code/session_019X3he87Ggr1qt8y7mnFeuE


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added Wi‑Fi radio status and control to the tools page, including “force disable” when wired/Ethernet isn’t detected.
    • Introduced a live System Diagnostics panel with periodic refresh (optimized to run only when the panel is visible).
    • Reworked power controls into a dedicated System Power section for reboot and shut down with confirmation prompts.
    • Added API support for Wi‑Fi radio GET/POST to power the new UI.

…s tab

Expands the web UI Tools tab with safe, purpose-built controls so users can
manage the Pi without SSHing in, instead of an arbitrary-command terminal
(the web UI has no auth and CSRF is disabled, so a shell would be unsafe).

- System Diagnostics card: renders the existing but previously-unused
  GET /api/v3/system/status endpoint (CPU, memory, temp, disk, uptime),
  with a manual refresh and a 10s poll.
- System Power section: reboot/shutdown buttons wired to the existing
  reboot_system / shutdown_system actions, behind a confirm step, with a
  dedicated powerAction() helper that treats the dropped connection as the
  expected "going offline" outcome rather than an error.
- Network Radio section: WiFi on/off toggle backed by new
  GET/POST /api/v3/wifi/radio endpoints and WiFiManager.set_wifi_radio() /
  get_wifi_radio_state(). Disabling WiFi is refused unless a wired
  connection is present (reusing the existing lockout guards), with an
  explicit force-off confirmation for advanced users.

No new privileged commands: uses nmcli radio wifi on|off (already
sudo-allowlisted) and the existing reboot/poweroff grants, so the
sudoers-alignment guard test stays green. Bluetooth toggle intentionally
omitted since the installer removes the BlueZ stack for LED timing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019X3he87Ggr1qt8y7mnFeuE
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 9 complexity · 2 duplication

Metric Results
Complexity 9
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a11d55e-b45f-4b43-8fa4-714ab2650dc5

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1722a and a75d349.

📒 Files selected for processing (1)
  • src/wifi_manager.py
📝 Walkthrough

Walkthrough

Adds WiFi radio state and control methods in WiFiManager, exposes them through new /wifi/radio API endpoints, and updates the tools UI with diagnostics polling, WiFi radio controls, and consolidated power actions.

Changes

WiFi Radio Control Feature

Layer / File(s) Summary
WiFiManager radio state and control methods
src/wifi_manager.py
Adds get_wifi_radio_state() for WiFi/Ethernet status reporting and set_wifi_radio(enabled, force=False) for controlled enable/disable operations with timeout and error codes.
WiFi radio API endpoints
web_interface/blueprints/api_v3.py
Adds GET /wifi/radio and POST /wifi/radio endpoints that read and update radio state through WiFiManager, with JSON parsing, coercion, and error handling.
Tools UI diagnostics, WiFi, and power controls
web_interface/templates/v3/partials/tools.html
Adds a System Diagnostics card, a Network Radio toggle with forced disable handling, and a unified System Power section, plus client logic for polling, rendering, and action dispatch.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ToolsUI as tools.html JS
  participant API as api_v3 /wifi/radio
  participant WiFiManager

  User->>ToolsUI: click wifi-radio-toggle
  ToolsUI->>API: POST /wifi/radio {enabled, force}
  API->>WiFiManager: set_wifi_radio(enabled, force)
  WiFiManager-->>API: (success, message, reason_code)
  alt disable refused without Ethernet
    API-->>ToolsUI: 400 response with reason=no_ethernet
    ToolsUI->>User: show wifi-radio-force-row
    User->>ToolsUI: confirm force disable
    ToolsUI->>API: POST /wifi/radio {enabled:false, force:true}
    API->>WiFiManager: set_wifi_radio(false, true)
    WiFiManager-->>API: refreshed radio state
  end
  API-->>ToolsUI: updated radio state
  ToolsUI->>User: update toggle and note
Loading

Possibly related PRs

  • ChuckBuilds/LEDMatrix#373: Both PRs modify the v3 tools UI partial and its JavaScript that dispatches system actions through /api/v3/system/action.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main Tools tab additions: diagnostics, power controls, and WiFi radio toggle.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/tools-tab-terminal-hfk9f3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
web_interface/templates/v3/partials/tools.html (1)

715-719: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Diagnostics polling isn't cleared when the user navigates away from the Tools tab.

window._diagPollInterval is only cleared/reset when this partial is re-swapped in; if the user switches to a different tab entirely, the interval keeps firing fetch('/api/v3/system/status') every 10s in the background (the if (!panel) return; guard in loadSystemDiagnostics prevents a JS error, but the network/server call still happens). On a resource-constrained Raspberry Pi, this is unnecessary CPU/network churn while the panel isn't even visible.

Consider clearing the interval on an HTMX "leaving this view" hook (e.g. htmx:beforeSwap on the outer container, or a MutationObserver/unload handler scoped to #tools-root) rather than only on re-entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web_interface/templates/v3/partials/tools.html` around lines 715 - 719, The
diagnostics polling interval is only stopped when the Tools partial is reloaded,
so it keeps running after the user leaves the tab. Update the polling setup
around window._diagPollInterval and loadSystemDiagnostics to also clear the
interval when the Tools view is being removed or swapped out, using an HTMX
view-leave hook on the outer container (for example htmx:beforeSwap) or an
equivalent scoped teardown tied to `#tools-root`.
src/wifi_manager.py (1)

1923-1972: 🚀 Performance & Scalability | 🔵 Trivial

Enable path can block the Flask worker for several seconds.

set_wifi_radio(True, ...) delegates to _ensure_wifi_radio_enabled(), whose retry loop can sleep for a total of ~10-20s across attempts (2s + 1s per retry, up to 3 retries, plus rfkill checks). Since this runs synchronously inside the /api/v3/wifi/radio POST request handler, a slow/failing radio-enable ties up the request thread for that duration. This mirrors the existing pattern in enable_ap_mode/disable_ap_mode (also synchronous), so it's not new to this PR, but worth flagging since a new UI surface now exercises it directly from a "quick toggle" control that users will expect to be near-instant.

Consider offloading long-running WiFi radio changes to a background thread with the endpoint returning immediately (with the UI polling for the new state), or documenting the expected multi-second latency in the toggle's UI copy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wifi_manager.py` around lines 1923 - 1972, The enable path in
set_wifi_radio(True, ...) can block the Flask request thread because it waits
inside _ensure_wifi_radio_enabled() with retries and sleeps. Update the
/api/v3/wifi/radio flow so the radio change is handled asynchronously in a
background worker/thread and the endpoint returns immediately, or add
UI/documentation copy that clearly sets expectations for the multi-second delay.
Use the existing set_wifi_radio and _ensure_wifi_radio_enabled symbols to keep
the fix aligned with the current enable/disable handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web_interface/blueprints/api_v3.py`:
- Around line 7207-7248: The set_wifi_radio endpoint in api_v3.py parses enabled
with a plain bool() cast, which can turn string inputs like "false" into True.
Update set_wifi_radio to use the same defensive string-to-bool handling already
used for force, so enabled only accepts real booleans or known truthy/falsey
string values. Keep the behavior consistent with the existing
WiFiManager.set_wifi_radio call and preserve the current success/error response
flow.

In `@web_interface/templates/v3/partials/tools.html`:
- Around line 665-696: The disable-without-force path in setWifiRadio is
treating every failure as the ethernet-safety refusal, which hides real backend
errors. Update the API response from set_wifi_radio / the wifi radio handler to
include a distinguishing field such as reason when the no-ethernet guard
triggers, and have setWifiRadio check that field before calling
showWifiForceRow. For non-no_ethernet failures, surface d.message through
showResult instead of showing the force-off confirmation UI; keep the existing
retry/loadWifiRadio flow unchanged.

---

Nitpick comments:
In `@src/wifi_manager.py`:
- Around line 1923-1972: The enable path in set_wifi_radio(True, ...) can block
the Flask request thread because it waits inside _ensure_wifi_radio_enabled()
with retries and sleeps. Update the /api/v3/wifi/radio flow so the radio change
is handled asynchronously in a background worker/thread and the endpoint returns
immediately, or add UI/documentation copy that clearly sets expectations for the
multi-second delay. Use the existing set_wifi_radio and
_ensure_wifi_radio_enabled symbols to keep the fix aligned with the current
enable/disable handling.

In `@web_interface/templates/v3/partials/tools.html`:
- Around line 715-719: The diagnostics polling interval is only stopped when the
Tools partial is reloaded, so it keeps running after the user leaves the tab.
Update the polling setup around window._diagPollInterval and
loadSystemDiagnostics to also clear the interval when the Tools view is being
removed or swapped out, using an HTMX view-leave hook on the outer container
(for example htmx:beforeSwap) or an equivalent scoped teardown tied to
`#tools-root`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c366e02-7110-4dfb-8c1d-34db7024ff7e

📥 Commits

Reviewing files that changed from the base of the PR and between 85d321c and 2d06530.

📒 Files selected for processing (3)
  • src/wifi_manager.py
  • web_interface/blueprints/api_v3.py
  • web_interface/templates/v3/partials/tools.html

Comment thread web_interface/blueprints/api_v3.py
Comment thread web_interface/templates/v3/partials/tools.html
claude added 2 commits July 9, 2026 11:43
Addresses code review feedback on the Tools tab additions:

- api_v3.py: parse the `enabled` POST field with the same string-aware
  coercion as `force`. A plain bool() cast turned {"enabled":"false"} into
  True (enabling instead of disabling) for any non-UI API caller.
- wifi_manager.set_wifi_radio() now returns a reason code alongside
  (success, message); the /wifi/radio error response includes it. The Tools
  UI only shows the force-off confirmation when reason == 'no_ethernet', so a
  genuine nmcli failure surfaces its real error instead of a misleading
  "no wired connection" prompt that would just retry into the same failure.
- tools.html: gate the 10s diagnostics poll on panel visibility
  (document.hidden / offsetParent), so switching to another tab stops the
  recurring /api/v3/system/status calls instead of churning the Pi off-screen.
  The initial load and manual Refresh remain unconditional.

Verified: {"enabled":"false"} now disables (refused w/ reason:no_ethernet),
{"enabled":"true"} enables; inline JS passes node --check; py_compile clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019X3he87Ggr1qt8y7mnFeuE
…inal-hfk9f3

# Conflicts:
#	web_interface/templates/v3/partials/tools.html

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/wifi_manager.py (1)

1970-1974: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add stack trace to the fallback error log and narrow the catch.

The trailing except Exception swallows all remaining error types and logs without a stack trace, which hampers remote debugging on the Pi. subprocess.run here realistically raises OSError/SubprocessError, so catching those (plus keeping a defensive fallback with exc_info=True) preserves context.

As per coding guidelines: "Catch specific exception types ... rather than generic exceptions" and "Include stack traces in error logs to provide complete error context for debugging".

♻️ Proposed change
-        except subprocess.TimeoutExpired:
-            return False, "Command timed out while disabling WiFi radio.", 'timeout'
-        except Exception as e:
-            logger.error("Error disabling WiFi radio: %s", e)
-            return False, "An error occurred while disabling WiFi radio.", 'error'
+        except subprocess.TimeoutExpired:
+            return False, "Command timed out while disabling WiFi radio.", 'timeout'
+        except (OSError, subprocess.SubprocessError) as e:
+            logger.error("Error disabling WiFi radio: %s", e, exc_info=True)
+            return False, "An error occurred while disabling WiFi radio.", 'error'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wifi_manager.py` around lines 1970 - 1974, The fallback error handling in
the WiFi radio disable path is too broad and loses traceback context. In the
code around the `subprocess.run` call and its surrounding `except` blocks,
replace the generic `except Exception` with narrower handling for the expected
`OSError`/`subprocess.SubprocessError` cases, and keep a defensive fallback that
logs with stack trace details. Update the `logger.error` call in this branch to
include exception info so remote debugging has the full context.

Sources: Coding guidelines, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/wifi_manager.py`:
- Around line 1970-1974: The fallback error handling in the WiFi radio disable
path is too broad and loses traceback context. In the code around the
`subprocess.run` call and its surrounding `except` blocks, replace the generic
`except Exception` with narrower handling for the expected
`OSError`/`subprocess.SubprocessError` cases, and keep a defensive fallback that
logs with stack trace details. Update the `logger.error` call in this branch to
include exception info so remote debugging has the full context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1a4e020-01b4-44c2-af31-14c66a95a62b

📥 Commits

Reviewing files that changed from the base of the PR and between 2d06530 and 2f1722a.

📒 Files selected for processing (3)
  • src/wifi_manager.py
  • web_interface/blueprints/api_v3.py
  • web_interface/templates/v3/partials/tools.html

Addresses a CodeRabbit nitpick: the fallback handler in set_wifi_radio()'s
disable path caught bare Exception and logged without a traceback. Narrow it to
(OSError, subprocess.SubprocessError) — the errors subprocess.run realistically
raises — and log with exc_info=True for full context on the Pi. Anything
genuinely unexpected now propagates to the endpoint's outer handler (500),
matching the codebase's specific-exception convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019X3he87Ggr1qt8y7mnFeuE
@ChuckBuilds ChuckBuilds merged commit bd9f461 into main Jul 9, 2026
8 checks passed
@ChuckBuilds ChuckBuilds deleted the claude/tools-tab-terminal-hfk9f3 branch July 9, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants